#!/usr/bin/ruby

STDOUT.sync = true

require 'yaml'
require 'uri'
require 'net/http'
require 'rexml/document'

class Ringr
  attr_reader :config

  def initialize
    Net::HTTP.version_1_2

    @config = YAML::load_file 'ini.yml'
    begin
      raise 'please edit ini.yml for api_key.' unless @config['api_key']
    rescue
      puts $!
      exit
    end

    @api = URI.parse @config['api_uri']
    @api_session = ''

    @counter = '';
    @room_ticket = ''

    @messages = []
    @occupants = []

    @view_messages = 50
    @timeout = 60

  end

  # create session
  def create_session
    param = {
      :api_key     => @config['api_key'],
      :client_type => 'human',
    }.collect{|set| set.join '='}.join '&'
    response = post("session/create" , param)
    @api_session = REXML::Document.new(response.body).elements.each("response/session"){|element| element}.pop.text
  end

  # destroy session
  def destroy_session
    param = {
      :session  => @api_session,
    }.collect{|set| set.join '='}.join '&'
    response = post("session/destroy" , param)
  end

  # auth login
  def auth_login
    if @config['login_email'] && @config['login_password']
      param = {
        :session  => @api_session,
        :email    => @config['login_email'],
        :password => @config['login_password'],
      }.collect{|set| set.join '='}.join '&'
      response = post("auth/login" , param)
    end
  end

  # enter room
  def enter_room room_no
    @room_no = room_no
    param = {
      :session => @api_session,
      :id      => @config['rooms'][@room_no]['id'],
    }
    param.merge!({:nickname => @config['rooms'][@room_no]['nickname']}) if @config['rooms'][@room_no]['nickname']
    param.merge!({:password => @config['rooms'][@room_no]['password']}) if @config['rooms'][@room_no]['password']
    query_string = param.collect{|set| set.join '='}.join '&'

    response = post("room/enter" , query_string)

    if REXML::Document.new(response.body).elements.each("response/room/requires_password"){|element| element}.pop.text == 'true'
      begin
        raise "This room required password! Please edit 'ini.yml' for password of this room." unless @config['rooms'][@room_no]['password']
      rescue
        puts $!
        exit
      end
    end

    @counter = REXML::Document.new(response.body).elements.each("response/room/counter"){|element| element}.pop.text
    @room_ticket = REXML::Document.new(response.body).elements.each("response/ticket"){|element| element}.pop.text

    REXML::Document.new(response.body).elements.each("response/occupants/occupant") do |element|
      occupant = {}
      element.each_child do |c|
        occupant[c.name] = c.text if c.class == REXML::Element
      end
      @occupants << occupant
    end
    get_archives
  end

  # usr.startObserving
  def user_start_observing
    param = {
      :session  => @api_session,
    }.collect{|set| set.join '='}.join '&'
    response = post("user/start_observing/" , param)
    @user_counter = REXML::Document.new(response.body).elements.each("response/counter"){|element| element}.pop.text
    @user_ticket = REXML::Document.new(response.body).elements.each("response/ticket"){|element| element}.pop.text
  end

  def user_observe
    param = {
      :session => @api_session,
      :ticket  => @user_ticket,
      :counter => @user_counter,
    }.collect{|set| set.join '='}.join '&'
    response = get("user/observe", param)
    @user_counter = REXML::Document.new(response.body).elements.each("response/counter"){|element| element}.pop.text
  end

  # enter room
  def exit_room
    param = {
      :session => @api_session,
      :ticket  => @room_ticket,
    }.collect{|set| set.join '='}.join '&'
    response = post("room/exit" , param)
  end

  # get archives
  def get_archives
    param = {
      :api_key => @config['api_key'],
      :id      => @config['rooms'][@room_no]['id'],
      :password => @config['rooms'][@room_no]['password'],
    }.collect{|set| set.join '='}.join '&'
    response = get("room/get_archives", param)

    REXML::Document.new(response.body).elements.each("response/messages/message") do |element|
      message = {}
      element.each_child do |c|
        if c.class == REXML::Element then
          c.text = DateTime.parse(c.text).strftime("%Y/%m/%d %H:%M:%S") if c.name == 'timestamp'
          message[c.name] = c.text
        end
      end
      @messages << message
      @messages.shift while @view_messages < @messages.size

    end
  end

  # say hello
  def say message
    param = {
      :session => @api_session,
      :ticket  => @room_ticket,
      :message => message,
    }.collect{|set| set.join '='}.join '&'
    response = post("room/say" , param)
  end

  # observe
  def observe
    param = {
      :session => @api_session,
      :ticket  => @room_ticket,
      :counter => @counter,
    }.collect{|set| set.join '='}.join '&'

    response = get("room/observe", param)
    response_xml = REXML::Document.new(response.body)

    @counter = response_xml.elements.each("response/counter"){|element| element}.pop.text

    response_xml.elements.each("response/messages/message") do |element|
      message = {}
      element.each_child do |c|
        if c.class == REXML::Element then
          c.text = DateTime.parse(c.text).strftime("%Y/%m/%d %H:%M:%S") if c.name == 'timestamp'
          message[c.name] = c.text
        end
      end
      @messages << message
      @messages.shift while @view_messages < @messages.size
    end

    new_occupants = []
    response_xml.elements.each("response/occupants/occupant") do |element|
      occupant = {}
      element.each_child do |c|
        occupant[c.name] = c.text if c.class == REXML::Element
      end
      new_occupants << occupant
    end
    @occupants = new_occupants if 0 < new_occupants.size

    view
  end

  def view
    system 'clear'

    # view messages
    @messages.each do |message|
      puts "[#{message['timestamp']}] #{message['nickname']}> #{message['text']}"
    end

    # view occupants
    puts "\n< Chatters >"
    @occupants.each do |occupant|
      if occupant['client_type'] == 'automaton'
        puts "[#{occupant['nickname']}]"
      else
        puts "#{occupant['nickname']}"
      end
    end

    # view prompt
    print "\n#{@config['rooms'][@room_no]['nickname']}@#{@config['rooms'][@room_no]['roomname']} (input 'exit' to leave)"
    print "\n> "

  end

  def get method, param
    begin
      Net::HTTP.start(@api.host, @api.port) do |http|
        http.read_timeout = @timeout
        response = http.get("#{@api.path}#{method}/?#{param}")
      end
    rescue Exception
      raise $! if $!.class == Timeout::Error
      log "exception on HTTP GET: #{$!}"
      nil
    end
  end

  def post method, param
    begin
      Net::HTTP.start(@api.host, @api.port) do |http| http.read_timeout = @timeout
        response = http.post("#{@api.path}#{method}/" , param)
      end
    rescue Exception
      raise $! if $!.class == Timeout::Error
      log "exception on HTTP POST: #{$!}"
      nil
    end
  end

  def log message
    File.open("#{File.dirname(__FILE__)}/error.log", 'a') do |log_file|
      log_file << "#{message}\n"
    end
  end

  # select room
  def select_room
    system 'clear'
    print <<-EOT
      _____________________________
     /                             \\
    (  Ringr::Lingr client by Ruby  )
     \\_____  ______________________/
           |/

    EOT
    @config['rooms'].each_with_index do |room, index|
      puts " #{index + 1}: #{room['roomname']}"
    end
    room_no = 0

    while room_no < 1 || @config['rooms'].size < room_no
      print "\nselect room (input \'exit\' to leave)"
      print "\n> "
      room_no = STDIN.gets.chomp

      case room_no
      when /^exit$/
        exit
      else
        room_no = room_no.to_i
      end
    end

    create_session
    auth_login

    enter_room(room_no - 1)

    view
  end

  # receiver thread
  def start_receiver_thread
    begin
      @receiver = Thread.new(self) do |r|
        observe = true
        while observe
          begin
            r.observe
          rescue Timeout::Error
#            r.log "Retry observe"
          end
        end
      end
    rescue Exception
      log $!
    end
  end

  # sender thread
  def start_sender_thread
    begin
      @sender = Thread.new do
        stay = true
        while stay
          message = STDIN.readline.chomp

          case message
          when /^exit$/
            stay = false
          else
            say message unless message.strip == ''
          end
        end
      end
    rescue Exception
      log $!
    end
  end

  # start method
  def go
    select_room

    start_sender_thread
    start_receiver_thread

    @sender.join # waiting for end of sender thread
    @receiver.exit

    exit_room
    destroy_session
  end

  ringr = Ringr.new
  ringr.go

end
